/*
  It's common to have a data interface which is used across multiple routes in an API,
  for instance a shared CSV Export system which can be applied to multiple entities in an application.

  By default this can present a challenge in tRPC clients, because the @trpc/react-query package 
  produces router interfaces which are not always considered structurally compatible by typescript.

  The polymorphism types can be used to generate abstract types which routers sharing a common 
  interface are compatible with, and allow you to pass around deep router paths to generic components with ease.
*/
import { getServerAndReactClient } from './__helpers';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { ReactNode } from 'react';
import React, { useState } from 'react';
import { describe, expect, test } from 'vitest';
import { z } from 'zod';
import type { InferOutput } from '../src';
import { t } from './polymorphism.common';
/**
 * We define a router factory which can be used many times.
 *
 * This also exports types generated by RouterLike and UtilsLike to describe
 * interfaces which concrete router instances are compatible with
 */
import * as Factory from './polymorphism.factory';

/**
 * The tRPC backend is defined here
 */
function createTRPCApi() {
  /**
   * Backend data sources.
   *
   * Here we use a simple array for demonstration, but in practice these might be
   * an ORM Repository, a microservice's API Client, etc. Whatever you write your own router factory around.
   */
  const IssueExportsProvider: Factory.FileExportStatusType[] = [];
  const DiscussionExportsProvider: Factory.FileExportStatusType[] = [];

  /**
   * Create an AppRouter instance, with multiple routes using the data export interface
   */
  const appRouter = t.router({
    github: t.router({
      issues: t.router({
        export: Factory.createExportRoute(t.procedure, IssueExportsProvider),
      }),
      discussions: t.router({
        export: t.mergeRouters(
          Factory.createExportRoute(t.procedure, DiscussionExportsProvider),

          // We want to be sure that routers with abstract types,
          //  which then get merged into a larger router, can be used polymorphically
          t.router({
            someExtraProcedure: t.procedure
              .input(z.object({ name: z.string().min(0) }))
              .mutation((opts) => {
                return 'Hello ' + opts.input.name;
              }),
          }),
        ),
      }),
    }),
  });

  return {
    t,
    appRouter,
    IssueExportsProvider,
    DiscussionExportsProvider,
  };
}

describe('polymorphism', () => {
  /**
   * Test setup
   */
  const testContext = () => {
    const { appRouter, IssueExportsProvider, DiscussionExportsProvider } =
      createTRPCApi();

    return Object.assign(getServerAndReactClient(appRouter), {
      IssueExportsProvider,
      DiscussionExportsProvider,
    });
  };

  describe('simple factory', () => {
    test('can use a simple factory router with an abstract interface', async () => {
      await using ctx = testContext();
      const { useTRPC } = ctx;

      /**
       * Can now define page components which re-use functionality from components,
       * and pass the specific backend functionality which is needed need
       */
      function IssuesExportPage() {
        const trpc = useTRPC();
        const client = useQueryClient();

        const [currentExport, setCurrentExport] = useState<number | null>(null);
        const invalidate = useMutation({
          mutationFn: () => client.invalidateQueries(trpc.github.queryFilter()),
        });

        return (
          <>
            <StartExportButton
              route={trpc.github.issues.export}
              onExportStarted={setCurrentExport}
            />

            <RefreshExportsListButton
              mutate={invalidate.mutate}
              isPending={invalidate.isPending}
            />

            <ExportStatus
              status={trpc.github.issues.export.status}
              currentExport={currentExport}
            />

            <ExportsList list={trpc.github.issues.export.list} />
          </>
        );
      }

      /**
       * Test Act & Assertions
       */

      const $ = ctx.renderApp(<IssuesExportPage />);

      await userEvent.click($.getByTestId('startExportBtn'));

      await waitFor(() => {
        expect($.container).toHaveTextContent(
          'Last Export: `Search for Polymorphism React` (Working)',
        );
      });

      await userEvent.click($.getByTestId('refreshBtn'));

      await waitFor(() => {
        expect($.container).toHaveTextContent(
          'Last Export: `Search for Polymorphism React` (Ready!)',
        );
      });
    });

    test('can use the abstract interface with a factory instance which has been merged with some extra procedures', async () => {
      await using ctx = testContext();
      const { useTRPC } = ctx;

      function DiscussionsExportPage() {
        const trpc = useTRPC();
        const client = useQueryClient();

        const [currentExport, setCurrentExport] = useState<number | null>(null);

        const invalidate = useMutation({
          mutationFn: () => client.invalidateQueries(trpc.github.queryFilter()),
        });

        return (
          <>
            <StartExportButton
              route={trpc.github.discussions.export}
              onExportStarted={setCurrentExport}
            />

            <RefreshExportsListButton
              mutate={invalidate.mutate}
              isPending={invalidate.isPending}
            />

            <ExportStatus
              status={trpc.github.discussions.export.status}
              currentExport={currentExport}
            />

            <ExportsList list={trpc.github.discussions.export.list} />
          </>
        );
      }

      /**
       * Test Act & Assertions
       */

      const $ = ctx.renderApp(<DiscussionsExportPage />);

      await userEvent.click($.getByTestId('startExportBtn'));

      await waitFor(() => {
        expect($.container).toHaveTextContent(
          'Last Export: `Search for Polymorphism React` (Working)',
        );
      });

      await userEvent.click($.getByTestId('refreshBtn'));

      await waitFor(() => {
        expect($.container).toHaveTextContent(
          'Last Export: `Search for Polymorphism React` (Ready!)',
        );
      });
    });
  });
});

/**
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * General use components which can consume any matching route interface
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 */

function StartExportButton(props: {
  route: Factory.ExportRouteLike;
  onExportStarted: (id: number) => void;
}) {
  const client = useQueryClient();

  const exportStarter = useMutation(
    props.route.start.mutationOptions({
      async onSuccess(data) {
        props.onExportStarted(data.id);

        await client.invalidateQueries(props.route.queryFilter());
      },
    }),
  );

  return (
    <button
      data-testid="startExportBtn"
      onClick={() => {
        exportStarter.mutate({
          filter: 'polymorphism react',
          name: 'Search for Polymorphism React',
        });
      }}
    >
      Start Export
    </button>
  );
}

function RefreshExportsListButton(props: {
  mutate: () => void;
  isPending: boolean;
}) {
  return (
    <button
      data-testid="refreshBtn"
      onClick={props.mutate}
      disabled={props.isPending}
    >
      Refresh
    </button>
  );
}

function ExportStatus<
  TStatus extends Factory.ExportRouteLike['status'],
>(props: {
  status: TStatus;
  renderAdditionalFields?: (data: InferOutput<TStatus>) => ReactNode;
  currentExport: number | null;
}) {
  const exportStatus = useQuery(
    props.status.queryOptions(
      { id: props.currentExport ?? -1 },
      { enabled: props.currentExport !== null },
    ),
  );

  if (!exportStatus.data) {
    return null;
  }

  return (
    <p>
      Last Export: `{exportStatus.data?.name}` (
      {exportStatus.data.downloadUri ? 'Ready!' : 'Working'})
      {props.renderAdditionalFields?.(exportStatus.data)}
    </p>
  );
}

function ExportsList(props: {
  list: Omit<Factory.ExportRouteLike['list'], 'nothing'>;
}) {
  const exportsList = useQuery(props.list.queryOptions());

  return (
    <>
      <h4>Downloads:</h4>
      <ul>
        {exportsList.data
          ?.map((item) =>
            item.downloadUri ? (
              <li key={item.id}>
                <a href={item.downloadUri ?? '#'}>{item.name}</a>
              </li>
            ) : null,
          )
          .filter(Boolean)}
      </ul>
    </>
  );
}
